리버싱

악성코드_03_패킹 메커니즘과 수동 언패킹 원리

작성자 : Heehyeon Yoo|2025-12-28
# 리버싱# 악성코드# 패킹# 언패킹# UPX

1. 패킹(Packing)과 프로텍터(Protector)

1.1 패킹(Packing)의 원리

패킹이란 말을 들으면 압축을 떠올리게 되는데, 사실은 압축 기술이라기 보다 실행 파일의 PE 구조(Portable Executable Structure)를 통째로 재구성하여 원본 코드를 숨기는 일종의 '컨테이너(Container)' 기법에 가깝다.

패킹된 악성코드가 실행될 때 어떤 일이 벌어지는지 이해하려면, 프로그램의 제어권이 넘어가는 순서를 따라가야 한다.

  1. 초기 진입(Entry Point):
    운영체제(OS) 로더는 원본 코드가 아닌 언패킹 스터브(Unpacking Stub)를 먼저 실행한다. 원본 코드는 아직 암호화된 데이터 덩어리에 불과하다.
  2. 복원 과정(Restoration):
    스터브가 실행되면서 자신의 몸체에 포함된 압축 데이터 영역을 읽어들이고, 이를 디코딩하여 메모리에 원본 기계어 코드를 풀어놓는다.
  3. 임포트 테이블 복구(IAT Resolution):
    단순히 코드만 푼다고 끝이 아니다. LoadLibraryGetProcAddress를 사용하여 원본 프로그램이 필요로 하는 API 주소들을 하나하나 찾아서 채워 넣는다.
  4. OEP 이동(Jump to OEP):
    모든 준비가 끝나면 스터브는 임무를 마치고 OEP(Original Entry Point)로 점프한다. 이때부터 비로소 우리가 아는 '진짜 프로그램'이 돌아가기 시작한다.

1.2 실행 압축 vs 프로텍터

초기의 패커(Packer)(예: UPX)는 단순 실행 압축이 목적이었기에 스터브 구조가 단순하고 복구도 쉬웠다. 하지만 악성코드 제작자들은 곧 분석을 방해하기 위한 더 강력한 도구를 원했고, 그 결과 프로텍터(Protector)가 탄생했다.

구분패커(Packer)프로텍터(Protector)
대표 도구UPX, ASPack, FSGThemida, VMProtect, Enigma
핵심 목적실행 파일 크기 감소지적재산권 보호 및 분석 방해
기술적 차이단순 압축 알고리즘(LZMA 등) 사용안티 디버깅(Anti-Debugging), 가상 머신 탐지(VM Detection), 코드 가상화(Code Virtualization), 다형성(Polymorphism) 등 복합 적용

프로텍터는 단순히 코드를 숨기는 것을 넘어, 디버거가 붙으면 스스로 종료해버리거나 가짜 코드를 실행시켜 분석가를 혼란에 빠뜨린다.

2. 정적 분석: 디버거를 켜기 전에 알 수 있는 것들

파일을 실행해보지 않고 PE 헤더만 훑어봐도 "이거 뭔가 이상한데?"라고 느낄 수 있는 몇 가지 강력한 시그니처들이 있다.

2.1 기형적인 섹션(Section) 구조

정상적인 컴파일러(MSVC, GCC)는 .text, .data 같은 표준 이름과 합리적인 크기를 가진다. 하지만 패커는 다르다.

  • 이름의 흔적: UPX0, UPX1 처럼 대놓고 패커 이름을 쓰거나, .vmp 같은 프로텍터 시그니처가 남는다. 아무 이름이 없는 빈 섹션이 있기도 하다.
  • 크기의 불일치 (Raw vs Virtual):
    패킹된 섹션은 파일 상태에서는 압축되어 작지만, 메모리에 로드될 때는 압축이 풀릴 거대한 공간이 필요하다.
    • SizeOfRawData: 0 (또는 매우 작음)
    • VirtualSize: 매우 큼
  • 위험한 권한(Characteristics):
    데이터 섹션 주제에 쓰기(Write) + 실행(Execute) 권한을 동시에 가진다면(E0... 플래그), 이는 100% 코드를 런타임에 풀어서 실행하겠다는 의도다.

2.2 텅 빈 임포트 테이블(IAT)

정상 파일이라면 수십 개의 API를 임포트하겠지만, 패킹된 파일의 IAT는 썰렁하다. 스터브가 작동하는 데 필요한 최소한의 도구, 즉 **LoadLibrary**와 GetProcAddress 딱 두 개만 덩그러니 있는 경우가 많다. 나머지 수많은 API들은 암호화된 데이터 속에 숨겨져 있다가 실행 중에 동적으로 로드된다.

2.3 엔트로피(Entropy) 경고

Detect It Easy(DIE) 같은 도구로 분석했을 때 그래프가 온통 붉은색(높음)이라면 주의해야 한다.

  • 3~5점: 일반적인 코드 (패턴이 있음)
  • 7점 이상: 압축되거나 암호화된 데이터 (완전한 난수)

3. 실전: UPX 수동 언패킹 가이드

자동 툴이 안 먹힐 때를 대비해, 디버거(x64dbg)로 직접 껍질을 벗기는 수동 언패킹(Manual Unpacking) 과정을 익혀둬야 한다. 가장 기초적인 ESP Trick을 예로 들어보자.

1단계: PUSHAD 찾기

파일을 열자마자 첫 명령어가 PUSHAD라면 반은 성공이다.
PUSHAD는 현재 레지스터 상태를 스택(Stack)에 백업하는 명령어다. 스터브가 제멋대로 레지스터를 쓰고 나서, 나중에 POPAD로 원상 복구한 뒤 OEP로 넘겨주겠다는 뜻이다. 이 복구 시점이 바로 언패킹이 끝나는 타이밍이다.

2단계: ESP Trick 설정 (Hardware Breakpoint)

  1. F7이나 F8로 PUSHAD를 실행한다.
  2. 스택 창(Stack View)에서 ESP 레지스터가 가리키는 주소를 찾는다.
  3. 그 주소에 하드웨어 접근 중단점(Hardware Breakpoint on Access)을 건다.

3단계: OEP 포착

F9(Run)를 눌러 실행하면, 복잡한 압축 해제 루프를 순식간에 지나쳐 POPAD 명령어 직후에서 멈출 것이다.
이때 바로 밑을 보면 Tail Jump(JMP 혹은 RET)가 보인다. 아주 멀리 떨어진 주소로 점프한다면, 그곳이 바로 OEP다.

4단계: 덤프 및 IAT 복구 (Dump & Fix)

OEP에 도착하면 코드가 정상적인 함수 프롤로그(PUSH EBP...)로 바뀐 것을 볼 수 있다. 이제 Scylla 같은 도구로 현재 메모리 상태를 덤프(Dump) 뜨고, 손상된 IAT를 복구(Fix IAT)해주면 번듯한 실행 파일 하나가 뚝딱 만들어진다.